Micron Document
🎖️GitЯра🎖️


Displaying Raw • Download

feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/kml/KmlToGeoJsonTest.kt renovate/fastlane-2.x-lockfile (056eed64) Text, 21.81 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.feature.map.kml

Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertContains
Tff7b72import T7ee787kotlin.test.assertEquals
Tff7b72import T7ee787kotlin.test.assertFalse
Tff7b72import T7ee787kotlin.test.assertNotNull
Tff7b72import T7ee787kotlin.test.assertNull
Tff7b72import T7ee787kotlin.test.assertTrue

T8b949e/**
* The KML reader, which is what lets an imported KML draw on the MapLibre map at all.
*
* Common code, so these run on the JVM and iOS as well as Android — the parser is xmlutil's, not the platform's.
*/
Tff7b72class T56d364KmlToGeoJsonTest Tb4b4b4{

Tff7b72private Tff7b72fun Td2a8ffconvertTb4b4b4(Te6edf3kmlTb4b4b4: Tffa657StringTb4b4b4)Tb4b4b4: Tffa657String? Tff7b72= Te6edf3KmlToGeoJsonTb4b4b4.Te6edf3convertTb4b4b4(Te6edf3kmlTb4b4b4)

Tf0883e@Test
Tff7b72fun Td2a8ff`a point placemark becomes a point feature`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3result Tff7b72=
Te6edf3assertNotNullTb4b4b4(
Te6edf3convertTb4b4b4(
Ta5d6ff"""
Ta5d6ff <kml><Document><Placemark>
<name>Repeater</name>
<Point><coordinates>-107.6197182,34.1320507,2137</coordinates></Point>
</Placemark></Document></kml>
Ta5d6ff"""Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)

Te6edf3assertContainsTb4b4b4(Te6edf3resultTb4b4b4, Ta5d6ff"""Ta5d6ff"Ta5d6fftypeTa5d6ff"Ta5d6ff:Ta5d6ff"Ta5d6ffFeatureCollectionTa5d6ff"""Ta5d6ff"Ta5d6ff)
assertContains(result, Ta5d6ff"Ta5d6ff"""Ta5d6fftypeTa5d6ff"Ta5d6ff:Ta5d6ff"Ta5d6ffPointTa5d6ff"""Ta5d6ff"Ta5d6ff)
// Altitude is dropped: nothing downstream reads it.
assertContains(result, Ta5d6ff"Ta5d6ff"""Ta5d6ffcoordinatesTa5d6ff"Ta5d6ff:[-107.6197182,34.1320507]Ta5d6ff"""Tb4b4b4)
Te6edf3assertContainsTb4b4b4(Te6edf3resultTb4b4b4, Ta5d6ff"""Ta5d6ff"Ta5d6fftitleTa5d6ff"Ta5d6ff:Ta5d6ff"Ta5d6ffRepeaterTa5d6ff"""Ta5d6ff"Ta5d6ff)
}

@Test
fun `a line placemark keeps its order and needs two positions`() {
val result =
assertNotNull(
convert(
Ta5d6ff"Ta5d6ff"Ta5d6ff"
Tff7b72<Te6edf3kmlTff7b72>Tff7b72<Te6edf3PlacemarkTff7b72>Tff7b72<Te6edf3LineStringTff7b72>
Tff7b72<Te6edf3coordinatesTff7b72>Tff7b72-T79c0ff1T79c0ff0T79c0ff7.62Tb4b4b4,T79c0ff3T79c0ff4.07 Tff7b72-T79c0ff1T79c0ff0T79c0ff7.60Tb4b4b4,T79c0ff3T79c0ff4.10 Tff7b72-T79c0ff1T79c0ff0T79c0ff7.59Tb4b4b4,T79c0ff3T79c0ff4.14Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>
Tff7b72<Tff7b72/Te6edf3LineStringTff7b72>Tff7b72<Tff7b72/Te6edf3PlacemarkTff7b72>Tff7b72<Tff7b72/Te6edf3kmlTff7b72>
Ta5d6ff"""Ta5d6ff,
),
)

assertContains(result, Ta5d6ff"""Ta5d6ff"Ta5d6fftypeTa5d6ff"Tb4b4b4:Ta5d6ff"Ta5d6ffLineStringTa5d6ff"Ta5d6ff"""Ta5d6ff)
assertContains(result, Ta5d6ff"""Tff7b72[Tff7b72-T79c0ff1T79c0ff0T79c0ff7.62Tb4b4b4,T79c0ff3T79c0ff4.07Tff7b72]Tb4b4b4,Tff7b72[Tff7b72-T79c0ff1T79c0ff0T79c0ff7.6Tb4b4b4,T79c0ff3T79c0ff4.1Tff7b72]Tb4b4b4,Tff7b72[Tff7b72-T79c0ff1T79c0ff0T79c0ff7.59Tb4b4b4,T79c0ff3T79c0ff4.14Tff7b72]Ta5d6ff"""Ta5d6ff)
}

@Test
fun `a one-position line is not emitted as a line`() {
assertNull(
convert(
Ta5d6ff"Ta5d6ff<kml><Placemark><LineString><coordinates>-107.62,34.07</coordinates></LineString></Placemark></kml>Ta5d6ff"Ta5d6ff,
),
)
}

@Test
fun `a polygon ring is closed even when the source leaves it open`() {
val result =
assertNotNull(
convert(
Ta5d6ff"""
Tff7b72<Te6edf3kmlTff7b72>Tff7b72<Te6edf3PlacemarkTff7b72>Tff7b72<Te6edf3PolygonTff7b72>Tff7b72<Te6edf3outerBoundaryIsTff7b72>Tff7b72<Te6edf3LinearRingTff7b72>
Tff7b72<Te6edf3coordinatesTff7b72>Tff7b72-T79c0ff1T79c0ff0T79c0ff7.62Tb4b4b4,T79c0ff3T79c0ff4.07 Tff7b72-T79c0ff1T79c0ff0T79c0ff7.59Tb4b4b4,T79c0ff3T79c0ff4.07 Tff7b72-T79c0ff1T79c0ff0T79c0ff7.59Tb4b4b4,T79c0ff3T79c0ff4.14Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>
Tff7b72<Tff7b72/Te6edf3LinearRingTff7b72>Tff7b72<Tff7b72/Te6edf3outerBoundaryIsTff7b72>Tff7b72<Tff7b72/Te6edf3PolygonTff7b72>Tff7b72<Tff7b72/Te6edf3PlacemarkTff7b72>Tff7b72<Tff7b72/Te6edf3kmlTff7b72>
Ta5d6ff"""Ta5d6ff,
),
)

assertContains(result, Ta5d6ff"""Ta5d6ff"Ta5d6fftypeTa5d6ff"Tb4b4b4:Ta5d6ff"Ta5d6ffPolygonTa5d6ff"Ta5d6ff"""Ta5d6ff)
// First position repeated at the end.
assertContains(result, Ta5d6ff"""Tff7b72[Tff7b72[Tff7b72[Tff7b72-T79c0ff1T79c0ff0T79c0ff7.62Tb4b4b4,T79c0ff3T79c0ff4.07Tff7b72]Tb4b4b4,Tff7b72[Tff7b72-T79c0ff1T79c0ff0T79c0ff7.59Tb4b4b4,T79c0ff3T79c0ff4.07Tff7b72]Tb4b4b4,Tff7b72[Tff7b72-T79c0ff1T79c0ff0T79c0ff7.59Tb4b4b4,T79c0ff3T79c0ff4.14Tff7b72]Tb4b4b4,Tff7b72[Tff7b72-T79c0ff1T79c0ff0T79c0ff7.62Tb4b4b4,T79c0ff3T79c0ff4.07Tff7b72]Tff7b72]Tff7b72]Ta5d6ff"""Ta5d6ff)
}

@Test
fun `a line crossing the antimeridian is cut rather than drawn around the world`() {
// RFC 7946 §3.1.9. Uncut, a two-degree hop between 179°E and 179°W draws as a line back across every
// meridian on Earth.
val result =
assertNotNull(
convert(
Ta5d6ff"""
Tff7b72<Te6edf3kmlTff7b72>Tff7b72<Te6edf3PlacemarkTff7b72>Tff7b72<Te6edf3LineStringTff7b72>
Tff7b72<Te6edf3coordinatesTff7b72>T79c0ff1T79c0ff7T79c0ff9Tb4b4b4,T79c0ff1T79c0ff0 Tff7b72-T79c0ff1T79c0ff7T79c0ff9Tb4b4b4,T79c0ff1T79c0ff0Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>
Tff7b72<Tff7b72/Te6edf3LineStringTff7b72>Tff7b72<Tff7b72/Te6edf3PlacemarkTff7b72>Tff7b72<Tff7b72/Te6edf3kmlTff7b72>
Ta5d6ff"""Ta5d6ff,
),
)

assertContains(result, Ta5d6ff"""Ta5d6ff"Ta5d6fftypeTa5d6ff"Tb4b4b4:Ta5d6ff"Ta5d6ffMultiLineStringTa5d6ff"Ta5d6ff"""Ta5d6ff)
assertContains(result, Ta5d6ff"""Tff7b72[Tff7b72[Tff7b72[T79c0ff1T79c0ff7T79c0ff9.0Tb4b4b4,T79c0ff1T79c0ff0.0Tff7b72]Tb4b4b4,Tff7b72[T79c0ff1T79c0ff8T79c0ff0.0Tb4b4b4,T79c0ff1T79c0ff0.0Tff7b72]Tff7b72]Tb4b4b4,Tff7b72[Tff7b72[Tff7b72-T79c0ff1T79c0ff8T79c0ff0.0Tb4b4b4,T79c0ff1T79c0ff0.0Tff7b72]Tb4b4b4,Tff7b72[Tff7b72-T79c0ff1T79c0ff7T79c0ff9.0Tb4b4b4,T79c0ff1T79c0ff0.0Tff7b72]Tff7b72]Tff7b72]Ta5d6ff"""Ta5d6ff)
}

@Test
fun `the cut interpolates the latitude where the line meets the meridian`() {
val result =
assertNotNull(
convert(
Ta5d6ff"""
Tff7b72<Te6edf3kmlTff7b72>Tff7b72<Te6edf3PlacemarkTff7b72>Tff7b72<Te6edf3LineStringTff7b72>
Tff7b72<Te6edf3coordinatesTff7b72>T79c0ff1T79c0ff7T79c0ff0Tb4b4b4,T79c0ff0 Tff7b72-T79c0ff1T79c0ff7T79c0ff0Tb4b4b4,T79c0ff2T79c0ff0Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>
Tff7b72<Tff7b72/Te6edf3LineStringTff7b72>Tff7b72<Tff7b72/Te6edf3PlacemarkTff7b72>Tff7b72<Tff7b72/Te6edf3kmlTff7b72>
Ta5d6ff"""Ta5d6ff,
),
)

// Halfway along the twenty degrees of longitude it actually travels, so halfway up the latitude too.
assertContains(result, Ta5d6ff"""Tff7b72[Tff7b72[Tff7b72[T79c0ff1T79c0ff7T79c0ff0.0Tb4b4b4,T79c0ff0.0Tff7b72]Tb4b4b4,Tff7b72[T79c0ff1T79c0ff8T79c0ff0.0Tb4b4b4,T79c0ff1T79c0ff0.0Tff7b72]Tff7b72]Tb4b4b4,Tff7b72[Tff7b72[Tff7b72-T79c0ff1T79c0ff8T79c0ff0.0Tb4b4b4,T79c0ff1T79c0ff0.0Tff7b72]Tb4b4b4,Tff7b72[Tff7b72-T79c0ff1T79c0ff7T79c0ff0.0Tb4b4b4,T79c0ff2T79c0ff0.0Tff7b72]Tff7b72]Tff7b72]Ta5d6ff"""Ta5d6ff)
}

@Test
fun `a line that stays put is left as a plain LineString`() {
val result =
assertNotNull(
convert(
Ta5d6ff"""
Tff7b72<Te6edf3kmlTff7b72>Tff7b72<Te6edf3PlacemarkTff7b72>Tff7b72<Te6edf3LineStringTff7b72>
Tff7b72<Te6edf3coordinatesTff7b72>Tff7b72-T79c0ff1T79c0ff0T79c0ff7.62Tb4b4b4,T79c0ff3T79c0ff4.07 Tff7b72-T79c0ff1T79c0ff0T79c0ff7.59Tb4b4b4,T79c0ff3T79c0ff4.14Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>
Tff7b72<Tff7b72/Te6edf3LineStringTff7b72>Tff7b72<Tff7b72/Te6edf3PlacemarkTff7b72>Tff7b72<Tff7b72/Te6edf3kmlTff7b72>
Ta5d6ff"""Ta5d6ff,
),
)

assertContains(result, Ta5d6ff"""Ta5d6ff"Ta5d6fftypeTa5d6ff"Tb4b4b4:Ta5d6ff"Ta5d6ffLineStringTa5d6ff"Ta5d6ff"""Ta5d6ff)
}

@Test
fun `a clockwise ring is turned to follow the right-hand rule`() {
// RFC 7946 §3.1.6 requires an exterior ring to wind counterclockwise. KML says nothing about winding, so
// exporters emit both and the ring has to be turned rather than trusted.
val result =
assertNotNull(
convert(
Ta5d6ff"""
Tff7b72<Te6edf3kmlTff7b72>Tff7b72<Te6edf3PlacemarkTff7b72>Tff7b72<Te6edf3PolygonTff7b72>Tff7b72<Te6edf3outerBoundaryIsTff7b72>Tff7b72<Te6edf3LinearRingTff7b72>
Tff7b72<Te6edf3coordinatesTff7b72>Tff7b72-T79c0ff1T79c0ff0T79c0ff7.62Tb4b4b4,T79c0ff3T79c0ff4.07 Tff7b72-T79c0ff1T79c0ff0T79c0ff7.59Tb4b4b4,T79c0ff3T79c0ff4.14 Tff7b72-T79c0ff1T79c0ff0T79c0ff7.59Tb4b4b4,T79c0ff3T79c0ff4.07Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>
Tff7b72<Tff7b72/Te6edf3LinearRingTff7b72>Tff7b72<Tff7b72/Te6edf3outerBoundaryIsTff7b72>Tff7b72<Tff7b72/Te6edf3PolygonTff7b72>Tff7b72<Tff7b72/Te6edf3PlacemarkTff7b72>Tff7b72<Tff7b72/Te6edf3kmlTff7b72>
Ta5d6ff"""Ta5d6ff,
),
)

assertContains(result, Ta5d6ff"""Tff7b72[Tff7b72[Tff7b72[Tff7b72-T79c0ff1T79c0ff0T79c0ff7.59Tb4b4b4,T79c0ff3T79c0ff4.07Tff7b72]Tb4b4b4,Tff7b72[Tff7b72-T79c0ff1T79c0ff0T79c0ff7.59Tb4b4b4,T79c0ff3T79c0ff4.14Tff7b72]Tb4b4b4,Tff7b72[Tff7b72-T79c0ff1T79c0ff0T79c0ff7.62Tb4b4b4,T79c0ff3T79c0ff4.07Tff7b72]Tb4b4b4,Tff7b72[Tff7b72-T79c0ff1T79c0ff0T79c0ff7.59Tb4b4b4,T79c0ff3T79c0ff4.07Tff7b72]Tff7b72]Tff7b72]Ta5d6ff"""Ta5d6ff)
}

@Test
fun `a polygon hole is ignored rather than drawn as a second shape`() {
val result =
assertNotNull(
convert(
Ta5d6ff"""
Tff7b72<Te6edf3kmlTff7b72>Tff7b72<Te6edf3PlacemarkTff7b72>Tff7b72<Te6edf3PolygonTff7b72>
Tff7b72<Te6edf3outerBoundaryIsTff7b72>Tff7b72<Te6edf3LinearRingTff7b72>
Tff7b72<Te6edf3coordinatesTff7b72>T79c0ff0Tb4b4b4,T79c0ff0 T79c0ff4Tb4b4b4,T79c0ff0 T79c0ff4Tb4b4b4,T79c0ff4 T79c0ff0Tb4b4b4,T79c0ff4 T79c0ff0Tb4b4b4,T79c0ff0Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>
Tff7b72<Tff7b72/Te6edf3LinearRingTff7b72>Tff7b72<Tff7b72/Te6edf3outerBoundaryIsTff7b72>
Tff7b72<Te6edf3innerBoundaryIsTff7b72>Tff7b72<Te6edf3LinearRingTff7b72>
Tff7b72<Te6edf3coordinatesTff7b72>T79c0ff1Tb4b4b4,T79c0ff1 T79c0ff2Tb4b4b4,T79c0ff1 T79c0ff2Tb4b4b4,T79c0ff2 T79c0ff1Tb4b4b4,T79c0ff2 T79c0ff1Tb4b4b4,T79c0ff1Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>
Tff7b72<Tff7b72/Te6edf3LinearRingTff7b72>Tff7b72<Tff7b72/Te6edf3innerBoundaryIsTff7b72>
Tff7b72<Tff7b72/Te6edf3PolygonTff7b72>Tff7b72<Tff7b72/Te6edf3PlacemarkTff7b72>Tff7b72<Tff7b72/Te6edf3kmlTff7b72>
Ta5d6ff"""Ta5d6ff,
),
)

assertEquals(1, Regex(Ta5d6ff"Ta5d6ff\"Ta5d6fftypeTa5d6ff\"Ta5d6ff:Ta5d6ff\"Ta5d6ffPolygonTa5d6ff\"Ta5d6ff"Ta5d6ff).findAll(result).count())
assertContains(result, Ta5d6ff"""Tff7b72[Tff7b72[Tff7b72[T79c0ff0.0Tb4b4b4,T79c0ff0.0Tff7b72]Tb4b4b4,Tff7b72[T79c0ff4.0Tb4b4b4,T79c0ff0.0Tff7b72]Tb4b4b4,Tff7b72[T79c0ff4.0Tb4b4b4,T79c0ff4.0Tff7b72]Tb4b4b4,Tff7b72[T79c0ff0.0Tb4b4b4,T79c0ff4.0Tff7b72]Tb4b4b4,Tff7b72[T79c0ff0.0Tb4b4b4,T79c0ff0.0Tff7b72]Tff7b72]Tff7b72]Ta5d6ff"""Ta5d6ff)
}

@Test
fun `kml colour bytes are reversed into css and split into an opacity`() {
val result =
assertNotNull(
convert(
Ta5d6ff"""
Tff7b72<Te6edf3kmlTff7b72>Tff7b72<Te6edf3DocumentTff7b72>
Tff7b72<Te6edf3Style Te6edf3idTff7b72=Ta5d6ff"Ta5d6ffrouteTa5d6ff"Tff7b72>Tff7b72<Te6edf3LineStyleTff7b72>Tff7b72<Te6edf3colorTff7b72>Te6edf3ff0000ffTff7b72<Tff7b72/Te6edf3colorTff7b72>Tff7b72<Te6edf3widthTff7b72>T79c0ff4Tff7b72<Tff7b72/Te6edf3widthTff7b72>Tff7b72<Tff7b72/Te6edf3LineStyleTff7b72>Tff7b72<Tff7b72/Te6edf3StyleTff7b72>
Tff7b72<Te6edf3PlacemarkTff7b72>Tff7b72<Te6edf3styleUrlTff7b72>#Te6edf3routeTff7b72<Tff7b72/Te6edf3styleUrlTff7b72>
Tff7b72<Te6edf3LineStringTff7b72>Tff7b72<Te6edf3coordinatesTff7b72>T79c0ff0Tb4b4b4,T79c0ff0 T79c0ff1Tb4b4b4,T79c0ff1Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>Tff7b72<Tff7b72/Te6edf3LineStringTff7b72>
Tff7b72<Tff7b72/Te6edf3PlacemarkTff7b72>
Tff7b72<Tff7b72/Te6edf3DocumentTff7b72>Tff7b72<Tff7b72/Te6edf3kmlTff7b72>
Ta5d6ff"""Ta5d6ff,
),
)

// aabbggrr: ff 00 00 ff is opaque red, not opaque blue.
assertContains(result, Ta5d6ff"""Ta5d6ff"Ta5d6ffstrokeTa5d6ff"Tb4b4b4:Ta5d6ff"Ta5d6ff#ff0000Ta5d6ff"Ta5d6ff"""Ta5d6ff)
assertContains(result, Ta5d6ff"""Ta5d6ff"Ta5d6ffstroke-opacityTa5d6ff"Tb4b4b4:T79c0ff1.000Ta5d6ff"""Ta5d6ff)
assertContains(result, Ta5d6ff"""Ta5d6ff"Ta5d6ffstroke-widthTa5d6ff"Tb4b4b4:T79c0ff4.0Ta5d6ff"""Ta5d6ff)
}

@Test
fun `a half-transparent poly fill carries its opacity`() {
val result =
assertNotNull(
convert(
Ta5d6ff"""
Tff7b72<Te6edf3kmlTff7b72>Tff7b72<Te6edf3DocumentTff7b72>
Tff7b72<Te6edf3Style Te6edf3idTff7b72=Ta5d6ff"Ta5d6ffzoneTa5d6ff"Tff7b72>Tff7b72<Te6edf3PolyStyleTff7b72>Tff7b72<Te6edf3colorTff7b72>T79c0ff7fT79c0ff0T79c0ff0fTe6edf3f00Tff7b72<Tff7b72/Te6edf3colorTff7b72>Tff7b72<Tff7b72/Te6edf3PolyStyleTff7b72>Tff7b72<Tff7b72/Te6edf3StyleTff7b72>
Tff7b72<Te6edf3PlacemarkTff7b72>Tff7b72<Te6edf3styleUrlTff7b72>#Te6edf3zoneTff7b72<Tff7b72/Te6edf3styleUrlTff7b72>Tff7b72<Te6edf3PolygonTff7b72>Tff7b72<Te6edf3outerBoundaryIsTff7b72>Tff7b72<Te6edf3LinearRingTff7b72>
Tff7b72<Te6edf3coordinatesTff7b72>T79c0ff0Tb4b4b4,T79c0ff0 T79c0ff1Tb4b4b4,T79c0ff0 T79c0ff1Tb4b4b4,T79c0ff1 T79c0ff0Tb4b4b4,T79c0ff0Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>
Tff7b72<Tff7b72/Te6edf3LinearRingTff7b72>Tff7b72<Tff7b72/Te6edf3outerBoundaryIsTff7b72>Tff7b72<Tff7b72/Te6edf3PolygonTff7b72>Tff7b72<Tff7b72/Te6edf3PlacemarkTff7b72>
Tff7b72<Tff7b72/Te6edf3DocumentTff7b72>Tff7b72<Tff7b72/Te6edf3kmlTff7b72>
Ta5d6ff"""Ta5d6ff,
),
)

assertContains(result, Ta5d6ff"""Ta5d6ff"Ta5d6fffillTa5d6ff"Tb4b4b4:Ta5d6ff"Ta5d6ff#00ff00Ta5d6ff"Ta5d6ff"""Ta5d6ff)
assertTrue(result.contains(Ta5d6ff"""Ta5d6ff"Ta5d6fffill-opacityTa5d6ff"Tb4b4b4:T79c0ff0.49Ta5d6ff"""Ta5d6ff) || result.contains(Ta5d6ff"""Ta5d6ff"Ta5d6fffill-opacityTa5d6ff"Tb4b4b4:T79c0ff0.50Ta5d6ff"""Ta5d6ff), result)
}

@Test
fun `fill zero means outline only`() {
val result =
assertNotNull(
convert(
Ta5d6ff"""
Tff7b72<Te6edf3kmlTff7b72>Tff7b72<Te6edf3DocumentTff7b72>
Tff7b72<Te6edf3Style Te6edf3idTff7b72=Ta5d6ff"Ta5d6ffoutlineTa5d6ff"Tff7b72>Tff7b72<Te6edf3PolyStyleTff7b72>Tff7b72<Te6edf3colorTff7b72>Te6edf3ff0000ffTff7b72<Tff7b72/Te6edf3colorTff7b72>Tff7b72<Te6edf3fillTff7b72>T79c0ff0Tff7b72<Tff7b72/Te6edf3fillTff7b72>Tff7b72<Tff7b72/Te6edf3PolyStyleTff7b72>Tff7b72<Tff7b72/Te6edf3StyleTff7b72>
Tff7b72<Te6edf3PlacemarkTff7b72>Tff7b72<Te6edf3styleUrlTff7b72>#Te6edf3outlineTff7b72<Tff7b72/Te6edf3styleUrlTff7b72>Tff7b72<Te6edf3PolygonTff7b72>Tff7b72<Te6edf3outerBoundaryIsTff7b72>Tff7b72<Te6edf3LinearRingTff7b72>
Tff7b72<Te6edf3coordinatesTff7b72>T79c0ff0Tb4b4b4,T79c0ff0 T79c0ff1Tb4b4b4,T79c0ff0 T79c0ff1Tb4b4b4,T79c0ff1 T79c0ff0Tb4b4b4,T79c0ff0Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>
Tff7b72<Tff7b72/Te6edf3LinearRingTff7b72>Tff7b72<Tff7b72/Te6edf3outerBoundaryIsTff7b72>Tff7b72<Tff7b72/Te6edf3PolygonTff7b72>Tff7b72<Tff7b72/Te6edf3PlacemarkTff7b72>
Tff7b72<Tff7b72/Te6edf3DocumentTff7b72>Tff7b72<Tff7b72/Te6edf3kmlTff7b72>
Ta5d6ff"""Ta5d6ff,
),
)

assertTrue(Ta5d6ff"""Ta5d6ff"Ta5d6fffillTa5d6ff"Tb4b4b4:Ta5d6ff"""Ta5d6ff !in result, result)
}

@Test
fun `a style map resolves through its normal pair`() {
val result =
assertNotNull(
convert(
Ta5d6ff"""
Tff7b72<Te6edf3kmlTff7b72>Tff7b72<Te6edf3DocumentTff7b72>
Tff7b72<Te6edf3Style Te6edf3idTff7b72=Ta5d6ff"Ta5d6ffplainTa5d6ff"Tff7b72>Tff7b72<Te6edf3LineStyleTff7b72>Tff7b72<Te6edf3colorTff7b72>Te6edf3ff00ff00Tff7b72<Tff7b72/Te6edf3colorTff7b72>Tff7b72<Tff7b72/Te6edf3LineStyleTff7b72>Tff7b72<Tff7b72/Te6edf3StyleTff7b72>
Tff7b72<Te6edf3StyleMap Te6edf3idTff7b72=Ta5d6ff"Ta5d6ffpairTa5d6ff"Tff7b72>
Tff7b72<Te6edf3PairTff7b72>Tff7b72<Te6edf3keyTff7b72>Te6edf3normalTff7b72<Tff7b72/Te6edf3keyTff7b72>Tff7b72<Te6edf3styleUrlTff7b72>#Te6edf3plainTff7b72<Tff7b72/Te6edf3styleUrlTff7b72>Tff7b72<Tff7b72/Te6edf3PairTff7b72>
Tff7b72<Te6edf3PairTff7b72>Tff7b72<Te6edf3keyTff7b72>Te6edf3highlightTff7b72<Tff7b72/Te6edf3keyTff7b72>Tff7b72<Te6edf3styleUrlTff7b72>#Te6edf3otherTff7b72<Tff7b72/Te6edf3styleUrlTff7b72>Tff7b72<Tff7b72/Te6edf3PairTff7b72>
Tff7b72<Tff7b72/Te6edf3StyleMapTff7b72>
Tff7b72<Te6edf3PlacemarkTff7b72>Tff7b72<Te6edf3styleUrlTff7b72>#Te6edf3pairTff7b72<Tff7b72/Te6edf3styleUrlTff7b72>
Tff7b72<Te6edf3LineStringTff7b72>Tff7b72<Te6edf3coordinatesTff7b72>T79c0ff0Tb4b4b4,T79c0ff0 T79c0ff1Tb4b4b4,T79c0ff1Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>Tff7b72<Tff7b72/Te6edf3LineStringTff7b72>
Tff7b72<Tff7b72/Te6edf3PlacemarkTff7b72>
Tff7b72<Tff7b72/Te6edf3DocumentTff7b72>Tff7b72<Tff7b72/Te6edf3kmlTff7b72>
Ta5d6ff"""Ta5d6ff,
),
)

assertContains(result, Ta5d6ff"""Ta5d6ff"Ta5d6ffstrokeTa5d6ff"Tb4b4b4:Ta5d6ff"Ta5d6ff#00ff00Ta5d6ff"Ta5d6ff"""Ta5d6ff)
}

@Test
fun `a description with quotes and newlines stays valid json`() {
val result =
assertNotNull(
convert(
Ta5d6ff"Ta5d6ff<kml><Placemark><name>A</name><description>say Ta5d6ff\"Ta5d6ffhiTa5d6ff\"Ta5d6ff\Ta5d6ffnthen go</description>Ta5d6ff"Ta5d6ff +
Ta5d6ff"Ta5d6ff<Point><coordinates>1,2</coordinates></Point></Placemark></kml>Ta5d6ff"Ta5d6ff,
),
)

assertContains(result, Ta5d6ff"""Te6edf3say \Ta5d6ff"Ta5d6ffhiTa5d6ff\"Ta5d6ff\Ta5d6ffnthen goTa5d6ff"Ta5d6ff"Ta5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`a description holding raw html elements does not take the import down`Tb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Valid XML, not CDATA: exporters do write nested markup here, and one such description used to abort the
T8b949e// whole document. The markup is dropped; the text and the geometry survive.
Tff7b72val Te6edf3result Tff7b72=
Te6edf3assertNotNullTb4b4b4(
Te6edf3convertTb4b4b4(
Ta5d6ff"Ta5d6ff<kml><Placemark><name>A</name><description>a <b>bold</b> claim</description>Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ff<Point><coordinates>1,2</coordinates></Point></Placemark></kml>Ta5d6ff"Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)

Te6edf3assertContainsTb4b4b4(Te6edf3resultTb4b4b4, Ta5d6ff"Ta5d6ffa bold claimTa5d6ff"Tb4b4b4)
Te6edf3assertContainsTb4b4b4(Te6edf3resultTb4b4b4, Ta5d6ff"Ta5d6ff[1.0,2.0]Ta5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`several placemarks all reach the collection`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3result Tff7b72=
Te6edf3assertNotNullTb4b4b4(
Te6edf3convertTb4b4b4(
Ta5d6ff"""
Ta5d6ff <kml><Document>
<Placemark><Point><coordinates>1,1</coordinates></Point></Placemark>
<Placemark><Point><coordinates>2,2</coordinates></Point></Placemark>
<Placemark><LineString><coordinates>3,3 4,4</coordinates></LineString></Placemark>
</Document></kml>
Ta5d6ff"""Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(T79c0ff3Tb4b4b4, Te6edf3RegexTb4b4b4(Ta5d6ff"Ta5d6ff\"Ta5d6fftypeTa5d6ff\"Ta5d6ff:Ta5d6ff\"Ta5d6ffFeatureTa5d6ff\"Ta5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3findAllTb4b4b4(Te6edf3resultTb4b4b4)Tb4b4b4.Te6edf3countTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`a point carries its packed icon path`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3result Tff7b72=
Te6edf3assertNotNullTb4b4b4(
Te6edf3convertTb4b4b4(
Ta5d6ff"""
Ta5d6ff <kml><Document>
<Style id=Ta5d6ff"Ta5d6fftowerTa5d6ff"Ta5d6ff><IconStyle><Icon>
<href>files/tower.png</href>
</Icon></IconStyle></Style>
<Placemark><styleUrl>#tower</styleUrl>
<Point><coordinates>-107.62,34.07</coordinates></Point>
</Placemark>
</Document></kml>
Ta5d6ff"""Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)

Te6edf3assertContainsTb4b4b4(Te6edf3resultTb4b4b4, Ta5d6ff"""Ta5d6ff"Ta5d6fficon-urlTa5d6ff"Ta5d6ff:Ta5d6ff"Ta5d6fffiles/tower.pngTa5d6ff"""Ta5d6ff"Ta5d6ff)
}

@Test
fun `a point strips a remote icon url`() {
val result =
assertNotNull(
convert(
Ta5d6ff"Ta5d6ff"Ta5d6ff"
Tff7b72<Te6edf3kmlTff7b72>Tff7b72<Te6edf3DocumentTff7b72>
Tff7b72<Te6edf3Style Te6edf3idTff7b72=Ta5d6ff"Ta5d6fftowerTa5d6ff"Tff7b72>Tff7b72<Te6edf3IconStyleTff7b72>Tff7b72<Te6edf3IconTff7b72>
Tff7b72<Te6edf3hrefTff7b72>Te6edf3httpsTb4b4b4:T8b949e//example.org/tower.png</href>
Tff7b72<Tff7b72/Te6edf3IconTff7b72>Tff7b72<Tff7b72/Te6edf3IconStyleTff7b72>Tff7b72<Tff7b72/Te6edf3StyleTff7b72>
Tff7b72<Te6edf3PlacemarkTff7b72>Tff7b72<Te6edf3styleUrlTff7b72>#Te6edf3towerTff7b72<Tff7b72/Te6edf3styleUrlTff7b72>
Tff7b72<Te6edf3PointTff7b72>Tff7b72<Te6edf3coordinatesTff7b72>Tff7b72-T79c0ff1T79c0ff0T79c0ff7.62Tb4b4b4,T79c0ff3T79c0ff4.07Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>Tff7b72<Tff7b72/Te6edf3PointTff7b72>
Tff7b72<Tff7b72/Te6edf3PlacemarkTff7b72>
Tff7b72<Tff7b72/Te6edf3DocumentTff7b72>Tff7b72<Tff7b72/Te6edf3kmlTff7b72>
Ta5d6ff"""Ta5d6ff,
),
)

assertFalse(Ta5d6ff"Ta5d6fficon-urlTa5d6ff"Ta5d6ff in result, result)
}

@Test
fun `a line does not carry an icon even when its style names one`() {
// An icon says how a point is drawn; on a route it is noise the renderer would have to filter out again.
val result =
assertNotNull(
convert(
Ta5d6ff"""
Tff7b72<Te6edf3kmlTff7b72>Tff7b72<Te6edf3DocumentTff7b72>
Tff7b72<Te6edf3Style Te6edf3idTff7b72=Ta5d6ff"Ta5d6ffrouteTa5d6ff"Tff7b72>Tff7b72<Te6edf3IconStyleTff7b72>Tff7b72<Te6edf3IconTff7b72>Tff7b72<Te6edf3hrefTff7b72>Te6edf3httpsTb4b4b4:T8b949e//example.org/x.png</href></Icon></IconStyle>
Tff7b72<Te6edf3LineStyleTff7b72>Tff7b72<Te6edf3colorTff7b72>Te6edf3ff0000ffTff7b72<Tff7b72/Te6edf3colorTff7b72>Tff7b72<Tff7b72/Te6edf3LineStyleTff7b72>Tff7b72<Tff7b72/Te6edf3StyleTff7b72>
Tff7b72<Te6edf3PlacemarkTff7b72>Tff7b72<Te6edf3styleUrlTff7b72>#Te6edf3routeTff7b72<Tff7b72/Te6edf3styleUrlTff7b72>
Tff7b72<Te6edf3LineStringTff7b72>Tff7b72<Te6edf3coordinatesTff7b72>Tff7b72-T79c0ff1T79c0ff0T79c0ff7.62Tb4b4b4,T79c0ff3T79c0ff4.07 Tff7b72-T79c0ff1T79c0ff0T79c0ff7.59Tb4b4b4,T79c0ff3T79c0ff4.14Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>Tff7b72<Tff7b72/Te6edf3LineStringTff7b72>
Tff7b72<Tff7b72/Te6edf3PlacemarkTff7b72>
Tff7b72<Tff7b72/Te6edf3DocumentTff7b72>Tff7b72<Tff7b72/Te6edf3kmlTff7b72>
Ta5d6ff"""Ta5d6ff,
),
)

assertFalse(Ta5d6ff"Ta5d6fficon-urlTa5d6ff"Ta5d6ff in result, result)
}

@Test
fun `a NetworkLink href is not mistaken for an icon`() {
// <Link><href> lives outside any IconStyle, which is the whole reason the reader keys on the enclosing tag.
val result =
convert(
Ta5d6ff"""
Tff7b72<Te6edf3kmlTff7b72>Tff7b72<Te6edf3DocumentTff7b72>
Tff7b72<Te6edf3NetworkLinkTff7b72>Tff7b72<Te6edf3LinkTff7b72>Tff7b72<Te6edf3hrefTff7b72>Te6edf3httpsTb4b4b4:T8b949e//example.org/more.kml</href></Link></NetworkLink>
Tff7b72<Te6edf3PlacemarkTff7b72>Tff7b72<Te6edf3PointTff7b72>Tff7b72<Te6edf3coordinatesTff7b72>Tff7b72-T79c0ff1T79c0ff0T79c0ff7.62Tb4b4b4,T79c0ff3T79c0ff4.07Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>Tff7b72<Tff7b72/Te6edf3PointTff7b72>Tff7b72<Tff7b72/Te6edf3PlacemarkTff7b72>
Tff7b72<Tff7b72/Te6edf3DocumentTff7b72>Tff7b72<Tff7b72/Te6edf3kmlTff7b72>
Ta5d6ff"""Ta5d6ff,
)

assertNotNull(result)
assertFalse(Ta5d6ff"Ta5d6fficon-urlTa5d6ff"Ta5d6ff in result, result)
}

@Test
fun `an empty icon href is treated as no icon at all`() {
val result =
assertNotNull(
convert(
Ta5d6ff"""
Tff7b72<Te6edf3kmlTff7b72>Tff7b72<Te6edf3DocumentTff7b72>
Tff7b72<Te6edf3Style Te6edf3idTff7b72=Ta5d6ff"Ta5d6ffblankTa5d6ff"Tff7b72>Tff7b72<Te6edf3IconStyleTff7b72>Tff7b72<Te6edf3IconTff7b72>Tff7b72<Te6edf3hrefTff7b72>Tff7b72<Tff7b72/Te6edf3hrefTff7b72>Tff7b72<Tff7b72/Te6edf3IconTff7b72>Tff7b72<Tff7b72/Te6edf3IconStyleTff7b72>Tff7b72<Tff7b72/Te6edf3StyleTff7b72>
Tff7b72<Te6edf3PlacemarkTff7b72>Tff7b72<Te6edf3styleUrlTff7b72>#Te6edf3blankTff7b72<Tff7b72/Te6edf3styleUrlTff7b72>
Tff7b72<Te6edf3PointTff7b72>Tff7b72<Te6edf3coordinatesTff7b72>Tff7b72-T79c0ff1T79c0ff0T79c0ff7.62Tb4b4b4,T79c0ff3T79c0ff4.07Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>Tff7b72<Tff7b72/Te6edf3PointTff7b72>
Tff7b72<Tff7b72/Te6edf3PlacemarkTff7b72>
Tff7b72<Tff7b72/Te6edf3DocumentTff7b72>Tff7b72<Tff7b72/Te6edf3kmlTff7b72>
Ta5d6ff"""Ta5d6ff,
),
)

assertFalse(Ta5d6ff"Ta5d6fficon-urlTa5d6ff"Ta5d6ff in result, result)
}

@Test
fun `an icon or label colour is not mistaken for the line colour`() {
// Google Earth exports carry these alongside a real LineStyle, or with no LineStyle at all.
val result =
assertNotNull(
convert(
Ta5d6ff"""
Tff7b72<Te6edf3kmlTff7b72>Tff7b72<Te6edf3DocumentTff7b72>
Tff7b72<Te6edf3Style Te6edf3idTff7b72=Ta5d6ff"Ta5d6ffpinTa5d6ff"Tff7b72>
Tff7b72<Te6edf3IconStyleTff7b72>Tff7b72<Te6edf3colorTff7b72>Te6edf3ff00ffffTff7b72<Tff7b72/Te6edf3colorTff7b72>Tff7b72<Te6edf3scaleTff7b72>T79c0ff1.2Tff7b72<Tff7b72/Te6edf3scaleTff7b72>Tff7b72<Tff7b72/Te6edf3IconStyleTff7b72>
Tff7b72<Te6edf3LabelStyleTff7b72>Tff7b72<Te6edf3colorTff7b72>Te6edf3ffffffffTff7b72<Tff7b72/Te6edf3colorTff7b72>Tff7b72<Tff7b72/Te6edf3LabelStyleTff7b72>
Tff7b72<Tff7b72/Te6edf3StyleTff7b72>
Tff7b72<Te6edf3PlacemarkTff7b72>Tff7b72<Te6edf3styleUrlTff7b72>#Te6edf3pinTff7b72<Tff7b72/Te6edf3styleUrlTff7b72>
Tff7b72<Te6edf3LineStringTff7b72>Tff7b72<Te6edf3coordinatesTff7b72>T79c0ff0Tb4b4b4,T79c0ff0 T79c0ff1Tb4b4b4,T79c0ff1Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>Tff7b72<Tff7b72/Te6edf3LineStringTff7b72>
Tff7b72<Tff7b72/Te6edf3PlacemarkTff7b72>
Tff7b72<Tff7b72/Te6edf3DocumentTff7b72>Tff7b72<Tff7b72/Te6edf3kmlTff7b72>
Ta5d6ff"""Ta5d6ff,
),
)

assertTrue(Ta5d6ff"""Ta5d6ff"Ta5d6ffstrokeTa5d6ff"Tb4b4b4:Ta5d6ff"""Ta5d6ff !in result, result)
}

@Test
fun `a line style is still read when an icon style sits beside it`() {
val result =
assertNotNull(
convert(
Ta5d6ff"""
Tff7b72<Te6edf3kmlTff7b72>Tff7b72<Te6edf3DocumentTff7b72>
Tff7b72<Te6edf3Style Te6edf3idTff7b72=Ta5d6ff"Ta5d6ffbothTa5d6ff"Tff7b72>
Tff7b72<Te6edf3IconStyleTff7b72>Tff7b72<Te6edf3colorTff7b72>Te6edf3ff00ffffTff7b72<Tff7b72/Te6edf3colorTff7b72>Tff7b72<Tff7b72/Te6edf3IconStyleTff7b72>
Tff7b72<Te6edf3LineStyleTff7b72>Tff7b72<Te6edf3colorTff7b72>Te6edf3ff0000ffTff7b72<Tff7b72/Te6edf3colorTff7b72>Tff7b72<Te6edf3widthTff7b72>T79c0ff3Tff7b72<Tff7b72/Te6edf3widthTff7b72>Tff7b72<Tff7b72/Te6edf3LineStyleTff7b72>
Tff7b72<Tff7b72/Te6edf3StyleTff7b72>
Tff7b72<Te6edf3PlacemarkTff7b72>Tff7b72<Te6edf3styleUrlTff7b72>#Te6edf3bothTff7b72<Tff7b72/Te6edf3styleUrlTff7b72>
Tff7b72<Te6edf3LineStringTff7b72>Tff7b72<Te6edf3coordinatesTff7b72>T79c0ff0Tb4b4b4,T79c0ff0 T79c0ff1Tb4b4b4,T79c0ff1Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>Tff7b72<Tff7b72/Te6edf3LineStringTff7b72>
Tff7b72<Tff7b72/Te6edf3PlacemarkTff7b72>
Tff7b72<Tff7b72/Te6edf3DocumentTff7b72>Tff7b72<Tff7b72/Te6edf3kmlTff7b72>
Ta5d6ff"""Ta5d6ff,
),
)

assertContains(result, Ta5d6ff"""Ta5d6ff"Ta5d6ffstrokeTa5d6ff"Tb4b4b4:Ta5d6ff"Ta5d6ff#ff0000Ta5d6ff"Ta5d6ff"""Ta5d6ff)
assertContains(result, Ta5d6ff"""Ta5d6ff"Ta5d6ffstroke-widthTa5d6ff"Tb4b4b4:T79c0ff3.0Ta5d6ff"""Ta5d6ff)
}

@Test
fun `a file with nothing mappable converts to nothing rather than an empty collection`() {
// An empty FeatureCollection would draw nothing but claim the import succeeded.
assertNull(convert(Ta5d6ff"Ta5d6ff<kml><Document><name>Empty</name></Document></kml>Ta5d6ff"Ta5d6ff))
assertNull(convert(Ta5d6ff"Ta5d6ffnot xml at allTa5d6ff"Ta5d6ff))
}

@Test
fun `unsupported kml features are skipped without losing the rest of the file`() {
val result =
assertNotNull(
convert(
Ta5d6ff"""
Tff7b72<Te6edf3kmlTff7b72>Tff7b72<Te6edf3DocumentTff7b72>
Tff7b72<Te6edf3GroundOverlayTff7b72>Tff7b72<Te6edf3nameTff7b72>Te6edf3ImageryTff7b72<Tff7b72/Te6edf3nameTff7b72>Tff7b72<Te6edf3IconTff7b72>Tff7b72<Te6edf3hrefTff7b72>Te6edf3xTb4b4b4.Te6edf3pngTff7b72<Tff7b72/Te6edf3hrefTff7b72>Tff7b72<Tff7b72/Te6edf3IconTff7b72>Tff7b72<Tff7b72/Te6edf3GroundOverlayTff7b72>
Tff7b72<Te6edf3NetworkLinkTff7b72>Tff7b72<Te6edf3LinkTff7b72>Tff7b72<Te6edf3hrefTff7b72>Te6edf3remoteTb4b4b4.Te6edf3kmlTff7b72<Tff7b72/Te6edf3hrefTff7b72>Tff7b72<Tff7b72/Te6edf3LinkTff7b72>Tff7b72<Tff7b72/Te6edf3NetworkLinkTff7b72>
Tff7b72<Te6edf3PlacemarkTff7b72>Tff7b72<Te6edf3PointTff7b72>Tff7b72<Te6edf3coordinatesTff7b72>T79c0ff5Tb4b4b4,T79c0ff6Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>Tff7b72<Tff7b72/Te6edf3PointTff7b72>Tff7b72<Tff7b72/Te6edf3PlacemarkTff7b72>
Tff7b72<Tff7b72/Te6edf3DocumentTff7b72>Tff7b72<Tff7b72/Te6edf3kmlTff7b72>
Ta5d6ff"""Ta5d6ff,
),
)

assertEquals(1, Regex(Ta5d6ff"Ta5d6ff\"Ta5d6fftypeTa5d6ff\"Ta5d6ff:Ta5d6ff\"Ta5d6ffFeatureTa5d6ff\"Ta5d6ff"Ta5d6ff).findAll(result).count())
assertContains(result, Ta5d6ff"""Ta5d6ff"Ta5d6ffcoordinatesTa5d6ff"Tb4b4b4:Tff7b72[T79c0ff5.0Tb4b4b4,T79c0ff6.0Tff7b72]Ta5d6ff"""Ta5d6ff)
}

@Test
fun `an inline placemark style is applied in preference to a styleUrl`() {
// KML lets a Placemark carry its own Style, and says it wins over a shared one. The top-level scan cannot see
// such a Style, because the placemark reader has already consumed to the Placemark's end tag.
val result =
assertNotNull(
convert(
Ta5d6ff"""
Tff7b72<Te6edf3kmlTff7b72>Tff7b72<Te6edf3DocumentTff7b72>
Tff7b72<Te6edf3Style Te6edf3idTff7b72=Ta5d6ff"Ta5d6ffsharedTa5d6ff"Tff7b72>Tff7b72<Te6edf3LineStyleTff7b72>Tff7b72<Te6edf3colorTff7b72>Te6edf3ff0000ffTff7b72<Tff7b72/Te6edf3colorTff7b72>Tff7b72<Tff7b72/Te6edf3LineStyleTff7b72>Tff7b72<Tff7b72/Te6edf3StyleTff7b72>
Tff7b72<Te6edf3PlacemarkTff7b72>
Tff7b72<Te6edf3styleUrlTff7b72>#Te6edf3sharedTff7b72<Tff7b72/Te6edf3styleUrlTff7b72>
Tff7b72<Te6edf3StyleTff7b72>Tff7b72<Te6edf3LineStyleTff7b72>Tff7b72<Te6edf3colorTff7b72>Te6edf3ff00ff00Tff7b72<Tff7b72/Te6edf3colorTff7b72>Tff7b72<Te6edf3widthTff7b72>T79c0ff4Tff7b72<Tff7b72/Te6edf3widthTff7b72>Tff7b72<Tff7b72/Te6edf3LineStyleTff7b72>Tff7b72<Tff7b72/Te6edf3StyleTff7b72>
Tff7b72<Te6edf3LineStringTff7b72>Tff7b72<Te6edf3coordinatesTff7b72>T79c0ff1Tb4b4b4,T79c0ff2 T79c0ff3Tb4b4b4,T79c0ff4Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>Tff7b72<Tff7b72/Te6edf3LineStringTff7b72>
Tff7b72<Tff7b72/Te6edf3PlacemarkTff7b72>
Tff7b72<Tff7b72/Te6edf3DocumentTff7b72>Tff7b72<Tff7b72/Te6edf3kmlTff7b72>
Ta5d6ff"""Ta5d6ff,
),
)

assertContains(result, Ta5d6ff"""Ta5d6ff"Ta5d6ffstrokeTa5d6ff"Tb4b4b4:Ta5d6ff"Ta5d6ff#00ff00Ta5d6ff"Ta5d6ff"""Ta5d6ff)
assertContains(result, Ta5d6ff"""Ta5d6ff"Ta5d6ffstroke-widthTa5d6ff"Tb4b4b4:T79c0ff4Ta5d6ff"""Ta5d6ff)
}

@Test
fun `an inline style is read without swallowing the geometry beside it`() {
val result =
assertNotNull(
convert(
Ta5d6ff"""
Tff7b72<Te6edf3kmlTff7b72>Tff7b72<Te6edf3DocumentTff7b72>Tff7b72<Te6edf3PlacemarkTff7b72>
Tff7b72<Te6edf3StyleTff7b72>Tff7b72<Te6edf3PolyStyleTff7b72>Tff7b72<Te6edf3colorTff7b72>Te6edf3ffffff00Tff7b72<Tff7b72/Te6edf3colorTff7b72>Tff7b72<Tff7b72/Te6edf3PolyStyleTff7b72>Tff7b72<Tff7b72/Te6edf3StyleTff7b72>
Tff7b72<Te6edf3PointTff7b72>Tff7b72<Te6edf3coordinatesTff7b72>T79c0ff7Tb4b4b4,T79c0ff8Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>Tff7b72<Tff7b72/Te6edf3PointTff7b72>
Tff7b72<Tff7b72/Te6edf3PlacemarkTff7b72>Tff7b72<Tff7b72/Te6edf3DocumentTff7b72>Tff7b72<Tff7b72/Te6edf3kmlTff7b72>
Ta5d6ff"""Ta5d6ff,
),
)

assertContains(result, Ta5d6ff"""Ta5d6ff"Ta5d6ffcoordinatesTa5d6ff"Tb4b4b4:Tff7b72[T79c0ff7.0Tb4b4b4,T79c0ff8.0Tff7b72]Ta5d6ff"""Ta5d6ff)
}

@Test
fun `non-finite coordinates are rejected rather than written into the json`() {
// toDoubleOrNull accepts these, and they are interpolated straight into the output — one of them would emit a
// bare NaN token and make the whole file unparseable, taking every other placemark down with it.
assertNull(
convert(
Ta5d6ff"Ta5d6ff<kml><Document><Placemark><Point><coordinates>NaN,5</coordinates></Point></Placemark></Document></kml>Ta5d6ff"Ta5d6ff,
),
)
assertNull(
convert(
Ta5d6ff"Ta5d6ff<kml><Document><Placemark><Point><coordinates>Infinity,-Infinity</coordinates></Point>Ta5d6ff"Ta5d6ff +
Ta5d6ff"Ta5d6ff</Placemark></Document></kml>Ta5d6ff"Ta5d6ff,
),
)
}

@Test
fun `out-of-range coordinates are rejected`() {
assertNull(
convert(
Ta5d6ff"Ta5d6ff<kml><Document><Placemark><Point><coordinates>200,10</coordinates></Point></Placemark></Document></kml>Ta5d6ff"Ta5d6ff,
),
)
assertNull(
convert(
Ta5d6ff"Ta5d6ff<kml><Document><Placemark><Point><coordinates>10,95</coordinates></Point></Placemark></Document></kml>Ta5d6ff"Ta5d6ff,
),
)
}

@Test
fun `a bad position does not discard the good positions beside it`() {
val result =
assertNotNull(
convert(
Ta5d6ff"""
Tff7b72<Te6edf3kmlTff7b72>Tff7b72<Te6edf3DocumentTff7b72>Tff7b72<Te6edf3PlacemarkTff7b72>
Tff7b72<Te6edf3LineStringTff7b72>Tff7b72<Te6edf3coordinatesTff7b72>T79c0ff1Tb4b4b4,T79c0ff2 Te6edf3NaNTb4b4b4,T79c0ff4 T79c0ff5Tb4b4b4,T79c0ff6Tff7b72<Tff7b72/Te6edf3coordinatesTff7b72>Tff7b72<Tff7b72/Te6edf3LineStringTff7b72>
Tff7b72<Tff7b72/Te6edf3PlacemarkTff7b72>Tff7b72<Tff7b72/Te6edf3DocumentTff7b72>Tff7b72<Tff7b72/Te6edf3kmlTff7b72>
Ta5d6ff"""Ta5d6ff,
),
)

assertContains(result, Ta5d6ff"""Tff7b72[Tff7b72[T79c0ff1.0Tb4b4b4,T79c0ff2.0Tff7b72]Tb4b4b4,Tff7b72[T79c0ff5.0Tb4b4b4,T79c0ff6.0Tff7b72]Tff7b72]Ta5d6ff"""Ta5d6ff)
}
}

Served by rngit 1.5.2 - Generated in 0.16s